今天我們繼續分享tips 6~10。
有時候,我們會寫一些輔助的function來判斷傳入值是否符合某些條件,符合回傳True,不符合回傳False。
# 06a的模式,利用if-else來實作。# 06a
def get_bool(iterable):
if len(iterable) > 10:
return True
else:
return False
if __name__ == '__main__':
print(get_bool(range(10)), get_bool(range(11))) # False, True
else的部份,其實可以省略,所以也可以寫成# 06b的模式。# 06b
def get_bool(iterable):
if len(iterable) > 10:
return True
return False
if __name__ == '__main__':
print(get_bool(range(10)), get_bool(range(11))) # False, True
len(iterable) > 10本身就會回傳True或是False,可以直接返回。所以就有了# 06c的模式。我們覺得# 06c這種模式俗又有力,推薦您使用。# 06c
def get_bool(iterable):
return len(iterable) > 10
if __name__ == '__main__':
print(get_bool(range(10)), get_bool(range(11))) # False, True
當我們有一個內含多個str的list要寫入檔案,且每個str需要自己獨立一行。
# 07a中的模式,手動於每次寫入時,加入\n。# 07a
text = ['abcde', '12345']
with open('file1.txt', 'w') as f:
for s in text:
f.write(s+'\n')
print接受一個file參數,預設是sys.stdout,我們可以偷龍轉鳳將open之後的f指定給file,讓print來幫我們自動加入\n,如# 07b所示。# 07b
text = ['abcde', '12345']
with open('file2.txt', 'w') as f:
for s in text:
print(s, file=f)
print可以接受多個參數,所以我們可以進一步改寫# 07b為# 07c,直接傳入*text,並指定sep='\n'來節省一個迴圈。# 07c的寫法善用built-in function,且相當pythonic,推薦給您。# 07c
text = ['abcde', '12345']
with open('file3.txt', 'w') as f:
print(*text, sep='\n', file=f)
最後我們做個檢查。# 07d中可以使用()將context manager分成多行的語法,為Python 3.10新添增的。一般而言,context manager的命名會偏長,有了這個語法幫忙後,code看起來會清楚不少。
# 07d
with (open('file1.txt') as f1,
open('file2.txt') as f2,
open('file3.txt') as f3):
print(f1.read() == f2.read() == f3.read()) # True
當有一個list包含一個tuple時,如[('d', 4)]的情況,該如何取得'd'及4呢?
一般的解法是先利用[('d', 4)][0]來拿到內層的('d', 4),然後使用d, four = ('d', 4)的tuple unpacking,如# 08a。
# 08a
from collections import Counter
iterable = 'a'*1 + 'b'*2 + 'c'*3 + 'd'*4
cnter = Counter(iterable)
# cnter.most_common(1) # [('d', 4)]
if __name__ == '__main__':
# tuple unpacking
d, four = cnter.most_common(1)[0]
print(f'{d=}, {four=}') # d='d', four=4
針對這種情況,我們會推薦使用list unpacking [(d, four)] = [('d', 4)],如# 08b。沒錯,[]也能放在等號的左側。於此處使用list unpacking的好處是可以免去最外層需使用list來indexing,且擁有類似像Rust的語法,可以驗證左右側整體型式是否相同(list包含一個tuple,且tuple內元素數量要左右相同)。
# 08b
from collections import Counter
iterable = 'a'*1 + 'b'*2 + 'c'*3 + 'd'*4
cnter = Counter(iterable)
# cnter.most_common(1) # [('d', 4)]
if __name__ == '__main__':
# list unpacking
[(d, four)] = cnter.most_common(1)
print(f'{d=}, {four=}') # d='d', four=4
此外list unpacking也支援像tuple unpacking一樣的*語法,如# 08c,來收集剩餘的元素。
# 08c
from collections import Counter
iterable = 'a'*1 + 'b'*2 + 'c'*3 + 'd'*4
cnter = Counter(iterable)
# cnter.most_common(1) # [('d', 4)]
if __name__ == '__main__':
# `*` can be used in list unpacking as well
[(a, *_)] = [('a', 'b', 'c')]
print(f'{a=}, {_=}') # a='a', _=['b', 'c']
list unpacking雖然少見,但在這種特別的情形,我們會推薦使用。畢竟因為比較少用的關係,當自己在讀code看到時,很容易會注意到,反而會格外留心檢查右側的整體型別,或許這也是件好事吧XD
一般大家會使用zip的情況,應該是需要對多個iterable打迴圈的時候才會用到,但我們發現zip搭配dict也有許多妙用。
當需要一個dict,其keys為a~z,而values為1~26,您會如何建立呢(註1)?
一般來說,您應該會利用dict-comprehension,如# 09a。
# 09a
from string import ascii_lowercase
if __name__ == '__main__':
# [str, int]
d = {k: v
for k, v in zip(ascii_lowercase, range(1, 27))}
但我們發覺# 09b這種寫法更加優雅。zip幫我們將ascii_lowercase與count(1)縫在一起,然後交給dict幫忙生成。短短一行,我們靈活地使用了zip與itertools.count。
zip的強大,不只在於縫製iterable,也可以體現於拆iterable。當您有一個dict,您會如何同時取得keys及values呢?
dict.keys()與dict.values()。keys, values = zip(*d.items())是一種拆掉d的方法。這個方法得到的keys與values是tuple型態,所以可以使用[]來取值。而dict.keys()得到的dict_keys型態與dict.values()得到的dict_values型態,皆無法使用[]。# 09b
from itertools import count
from string import ascii_lowercase
if __name__ == '__main__':
# [str, int]
d = dict(zip(ascii_lowercase, count(1)))
keys, values = zip(*d.items())
continue減少縮排continue是一個於迴圈中離開當下這個cycle,進入下一個cycle的語法。
假設我們想要寫一個my_filter function(註2),其要求如下:
func及iterable。func會針對iterable中每個element給出True或False
my_filter會以generator型式返回func判斷為True的元素。一般來說,我們會寫出如# 10a的模式。但此時我們由於for與if,程式的主邏輯yield item得出現於第三層。如果外面層數過多,例如有使用context manager、多個迴圈或多個if,整體程式碼會很難閱讀。
# 10a
def func(x):
try:
return x > 10
except TypeError:
return False
def my_filter(func, iterable):
"""`yield item` locates at the second level of indentation"""
for item in iterable:
if func(item):
yield item
if __name__ == '__main__':
flt = my_filter(func, [2, 11, 'str', (), []])
print(list(flt)) # [11]
如果我們搭配not與continue,則會寫出# 10b的模式。這麼一來我們就只縮排了for這一層,程式的主邏輯yield item可以出現於第二層。
# 10b
def func(x):
try:
return x > 10
except TypeError:
return False
def my_filter(func, iterable):
"""`yield item` locates at the first level of indentation
by using `continue` to save one level of indentation"""
for item in iterable:
if not func(item):
continue
yield item
if __name__ == '__main__':
flt = my_filter(func, [2, 11, 'str', (), []])
print(list(flt)) # [11]
# 10a的方式較為直觀,但如若程式碼已縮排多層又很難重構的話,可以嘗試採用# 10b的寫法。
註1:如果想要反過來,建立一個keys為1~26,values為a-z的dict,可以這麼做:
# 09c
from string import ascii_lowercase
if __name__ == '__main__':
# [int, str]
d = dict(enumerate(ascii_lowercase, 1))
註2:Python的filter實作大致如下,我們實作的是個閹割的版本。
# 10c
def my_filter(func, iterable):
"""Emulate built-in filter"""
if func is not None:
return (item for item in iterable if func(item))
return (item for item in iterable if item)